Skip to content

fix(security): contain sandbox paths and output - #931

Draft
seonghobae wants to merge 15 commits into
mainfrom
fix/sandboxed-verify-symlink-boundary-20260811
Draft

fix(security): contain sandbox paths and output#931
seonghobae wants to merge 15 commits into
mainfrom
fix/sandboxed-verify-symlink-boundary-20260811

Conversation

@seonghobae

@seonghobae seonghobae commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Buyer and security outcome

This draft now closes two repository-owned availability and filesystem boundaries in the central sandbox wrappers without weakening execution, review, or merge policy:

  1. copied verification workspaces reject absolute and repository-escaping symlink targets before an untrusted command runs; and
  2. short-lived command streams plus long-running backend/frontend logs are continuously drained into explicit bounded final-suffix evidence.

Issue #766 remains open until protected integration and protected-main operational acceptance. The separately reviewed credential-redaction line in #906 is not claimed as shipped or bundled here.

Exact identity

  • source head: dd2132b5aedc56859ffa81568d42ab9e81e52b65
  • PR-base snapshot: 6eb06cdd08c79a06f7b390069d4ffa49e2eb7dba
  • independently resolved live main tip: 6eb06cdd08c79a06f7b390069d4ffa49e2eb7dba
  • ancestry: ahead 15, behind 0; merge base equals live main

No predecessor-head, synthetic merge revision, queued workflow, status-only result, automated model result, or author action transfers to this head.

Symlink containment

copy_workspace validates the exact copied tree after ignore rules are applied. Absolute links and relative links resolving outside the copied repository fail closed. Internal relative links remain links; ignored paths do not create false positives. This is filesystem containment, not an operating-system or network sandbox claim.

The original RED commit was faca1f145f237ce7b561218d707a40ae33471b88; the ignored-path regression was preserved at 6f597306a7414e4ab027af42c8d8672f8da8de39.

Output-resource RCA and remedy

The first failing boundary was retention: subprocess.run(..., stdout=PIPE, stderr=PIPE) buffered complete short-lived streams in parent memory, services wrote ordinary unbounded log files, and tail_text() read complete files before selecting final lines.

Rejected remedies:

  • process-wide RLIMIT_FSIZE, because it would also cap legitimate coverage databases, build artifacts, archives, and application files;
  • truncating only after communicate() or read_text(), because exhaustion occurs before that boundary;
  • retaining only the beginning of output, because terminal diagnostic suffixes are more useful;
  • non-POSIX silent fallback without process-group authority.

Implemented:

  • reusable bounded_subprocess.py with one binary reader thread per pipe, locked final-suffix buffers, one overflow transition, and finite 30-second reader joins;
  • 1 MiB default per stdout/stderr stream, 4 MiB per combined service log, and validation from 4 KiB through 64 MiB;
  • shell=False, structured argv, start_new_session=True, and whole-process-group termination on the first overflowing stream;
  • stable resource exit 123, timeout 124, and readiness 125, with timeout retaining precedence;
  • bounded persisted service evidence and a 64 KiB seek-from-end tail read;
  • explicit fail-closed behavior when POSIX process-group termination is unavailable;
  • stable copied-workspace path-boundary exit 126 with non-sensitive path_boundary_rejected evidence;
  • forced process-group kill and bounded reap if orderly service capture finalization raises.

The test-only RED head b4547a55a732f3f2f6b64e5924bca11e10871599 failed collection because the bounded runner did not exist. GREEN is the exact current head.

Exact local proof

At the exact current tree:

  • focused path-boundary, process-cleanup, and capture-ownership suite: 27 passed;
  • complete repository suite: 1,036 passed plus 16 subtests;
  • owned production coverage: 7,314/7,314 statements and 2,880/2,880 branches, 100%;
  • changed public production objects: 52/52 documented, 100%;
  • compilation and git diff --check: pass.

Tests exercise isolated platform-capability probes, missing-file budget validation, the exact service evidence ceiling, real stdout and stderr floods, service-log overflow before E2E sentinel execution, Unicode and partial UTF-8 suffixes, real readiness-synchronized ordinary success, exact 4 KiB boundary behavior, timeouts, reader errors, stuck readers, sibling finalization, capture-startup cleanup, unsupported platforms, invalid budgets, bounded kept-sandbox files, symlink containment, and exit-code precedence.

Governance, limitations, and rollback

Environment scrubbing, readiness SSRF controls, exact-head evidence, semantic review, independent approval, and branch protection remain separate authorities. This slice does not cap repository-copy size, application artifacts, CPU beyond existing timeouts, process count, address space, network traffic, or unrelated processes. A descendant that creates a new session can escape process-group termination, but finite reader joins convert retained descriptors into deterministic failure instead of an unbounded workflow wait.

Rollback requires a separately reviewed replacement proving bounded parent memory, bounded service evidence files, finite reader finalization, timeout/cleanup behavior, and realistic flood resistance.

Merge gate

Keep Draft until every required exact-head CI/security/supply-chain workflow is terminal-success, semantic review has no valid unresolved finding, live-base compatibility is refetched, qualifying independent non-author formal approval exists where required, and repository protection permits integration. After merge, run protected-main command and service flood canaries before closing #766.

Summary by CodeRabbit

  • 새로운 기능

    • 샌드박스 검증 및 웹 E2E 실행에 stdout/stderr와 서비스 로그의 출력 크기 제한을 추가했습니다.
    • 출력 초과 시 관련 프로세스를 안전하게 종료하고 결과에 제한 상태를 표시합니다.
    • 저장소 외부로 연결되는 위험한 심볼릭 링크를 실행 전에 차단합니다.
    • 타임아웃 발생 시에도 제한된 출력과 증거를 보존합니다.
  • 문서

    • 출력·실행 시간 제한 및 심볼릭 링크 보안 정책을 문서화했습니다.
  • 버그 수정

    • 캡처 초기화와 정리 실패를 안전하게 처리하고, 우선순위에 따른 종료 코드를 유지합니다.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@seonghobae, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 16 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: e516d668-4339-4956-b1b4-9c49e0fc6018

📥 Commits

Reviewing files that changed from the base of the PR and between 52e1281 and dd2132b.

📒 Files selected for processing (12)
  • CHANGELOG.md
  • docs/doctoring/sandboxed-output-resource-bounds.md
  • docs/doctoring/sandboxed-verification-symlink-boundary.md
  • scripts/ci/bounded_subprocess.py
  • scripts/ci/sandboxed_verify.py
  • scripts/ci/sandboxed_web_e2e.py
  • tests/test_bounded_subprocess.py
  • tests/test_bounded_subprocess_contract.py
  • tests/test_sandboxed_verify_output_limits.py
  • tests/test_sandboxed_verify_symlink_boundary.py
  • tests/test_sandboxed_web_e2e_branch_contract.py
  • tests/test_sandboxed_web_e2e_output_limits.py
📝 Walkthrough

Walkthrough

샌드박스 명령과 웹 E2E 서비스에 출력·로그 바이트 제한을 추가했습니다. 초과 시 프로세스 그룹을 종료하고 종료 코드 123을 반환합니다. timeout 및 readiness 우선순위를 유지합니다. bounded 증거 읽기와 심볼릭 링크 경계 검증도 추가했습니다.

Changes

샌드박스 자원 제한

Layer / File(s) Summary
bounded subprocess 실행 계약
scripts/ci/bounded_subprocess.py, tests/test_bounded_subprocess*.py, CHANGELOG.md, docs/doctoring/sandboxed-output-resource-bounds.md
POSIX 프로세스 그룹에서 명령을 실행합니다. stdout/stderr를 bounded suffix로 수집합니다. 출력 초과, timeout, reader 오류 및 정리 실패를 처리합니다.
sandboxed_verify 출력 및 경로 경계
scripts/ci/sandboxed_verify.py, tests/test_sandboxed_verify_*.py, tests/test_sandboxed_entrypoint_and_cleanup_coverage.py, docs/doctoring/sandboxed-verification-symlink-boundary.md
검증 명령에 출력 예산을 전달합니다. 결과에 제한 상태를 기록합니다. 저장소 외부를 가리키는 심볼릭 링크를 실행 전에 거부합니다.
웹 E2E 서비스 및 명령 통합
scripts/ci/sandboxed_web_e2e.py, tests/test_sandboxed_web_e2e.py, tests/test_repository_branch_coverage_execution_sandboxes.py
서비스 로그와 E2E 출력을 bounded capture로 수집합니다. readiness, timeout, 정리 및 결과 JSON에 제한 상태를 연결합니다.
웹 E2E 계약 및 실패 경로 검증
tests/test_sandboxed_web_e2e_branch_contract.py, tests/test_sandboxed_web_e2e_output_limits.py, tests/test_sandboxed_service_capture_startup.py, tests/test_sandboxed_entrypoint_and_cleanup_coverage.py
서비스 캡처 초기화·종료, 출력 초과, bounded tail, fail-closed 동작 및 종료 코드 우선순위를 검증합니다.

Estimated code review effort: 5 (Critical) | ~100 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: opencode-agent

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed Docstring coverage is 81.33% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 샌드박스 경로 격리와 출력 제한이라는 변경사항의 핵심을 간결하고 정확하게 설명합니다.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/sandboxed-verify-symlink-boundary-20260811

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review the unchanged exact source head ee903b875a0794f196060335231d9165e45000ea. All eight hosted CI/security/supply-chain workflows are terminal-success on this head. Keep semantic source findings separate from infrastructure and policy readiness; this request does not confer merge approval.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

@seonghobae I will review source head ee903b875a0794f196060335231d9165e45000ea.

I will report semantic source findings separately from infrastructure and policy readiness. This review does not approve the pull request.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@seonghobae seonghobae changed the title fix(security): contain verifier symlink targets fix(security): contain sandbox paths and output Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Please review exact source head 52e128145eddf2b6d1a2137c7e6406103c54db17. All ten hosted exact-head CI/security/supply-chain workflows, including Strix Changed Path Quality CI run 31457914791, are terminal-success. Treat prior-head or rate-limited evidence as non-authoritative; keep this Draft and do not infer independent human approval.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

@seonghobae I will review source head 52e128145eddf2b6d1a2137c7e6406103c54db17.

I will treat only exact-head evidence as authoritative. I will keep semantic source findings separate from CI, security, supply-chain, and policy readiness. This review does not approve the draft pull request or infer independent human approval.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (11)
scripts/ci/bounded_subprocess.py (1)

109-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

도달 불가능한 분기가 100% 분기 커버리지 요구와 충돌할 수 있습니다.

validate_output_limit는 최소 한도를 MINIMUM_OUTPUT_LIMIT_BYTES(4,096)로 강제합니다. TRUNCATION_MARKER는 그보다 훨씬 짧습니다. 따라서 suffix_budget은 검증된 한도에서 항상 양수입니다. if suffix_budget else b""의 거짓 분기는 정상 경로에서 실행되지 않습니다.

scripts/ci/ 코드에 100% 분기 커버리지를 유지해야 하므로, 이 분기를 직접 호출하는 단위 테스트를 추가하거나 분기를 제거하십시오.

As per coding guidelines: "Maintain 100% test coverage and 100% interrogate docstring coverage for code under scripts/ci/".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ci/bounded_subprocess.py` around lines 109 - 117, The suffix_budget
zero branch in _render_bounded_bytes is unreachable after validate_output_limit
enforces MINIMUM_OUTPUT_LIMIT_BYTES. Remove the conditional fallback and
construct the suffix using the guaranteed-positive budget, preserving the
marker-plus-suffix truncation behavior and 100% branch coverage.

Source: Coding guidelines

scripts/ci/sandboxed_verify.py (3)

288-288: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

타입이 확정된 결과에는 getattr 기본값이 필요하지 않습니다.

completedbounded_subprocess.BoundedCompletedProcess입니다. 이 dataclass는 output_limited: bool 필드를 항상 가집니다. getattr(completed, "output_limited", False)의 기본값은 실행되지 않으며, 나중에 계약이 깨져도 조용히 False로 처리합니다.

312행의 getattr은 다릅니다. 해당 except는 기반 클래스 subprocess.TimeoutExpired를 잡고, 그 클래스에는 output_limited가 없습니다. 그곳의 기본값은 유지하십시오.

♻️ 제안 리팩터
-            output_limited = bool(getattr(completed, "output_limited", False))
+            output_limited = completed.output_limited
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ci/sandboxed_verify.py` at line 288, Update the assignment in the
completed-process handling path to access the guaranteed output_limited field
directly on completed instead of using getattr with a fallback. Leave the
getattr usage in the TimeoutExpired exception handler unchanged, since that
handler receives a base exception without this field.

159-188: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

구현은 정확합니다. 실행 시점에 생성되는 링크에 대한 한계를 문서화하십시오.

경계 검사 자체는 견고합니다. os.walk(..., followlinks=False)는 링크된 디렉터리로 내려가지 않으므로 candidate.parent의 모든 구성 요소는 source_root 아래의 실제 디렉터리입니다. 따라서 (candidate.parent / target).resolve(strict=False)는 신뢰할 수 있는 기준점에서 시작합니다. 링크 체인도 resolve가 중간 링크를 따라가므로 각 링크가 개별적으로 차단됩니다. 절대 링크를 거부하는 결정도 복사본이 원본 체크아웃을 다시 가리키는 경우를 막습니다.

한 가지 경계 조건이 남습니다. 이 검증은 명령 실행 전에 한 번만 실행됩니다. 신뢰할 수 없는 검증 명령은 실행 중에 복사본 안에 탈출 심볼릭 링크를 새로 만들고 그것을 따라갈 수 있습니다. 이는 사전 검사로는 막을 수 없는 고유한 한계입니다. docs/doctoring/sandboxed-verification-symlink-boundary.md에 이 한계를 명시하면 이후 검토자가 이 통제를 실행 시점 봉쇄로 오해하지 않습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ci/sandboxed_verify.py` around lines 159 - 188, Document the runtime
symlink limitation in docs/doctoring/sandboxed-verification-symlink-boundary.md:
explain that validate_repository_symlinks performs only a pre-execution scan, so
an untrusted verification command may create and follow an escaping symlink
during execution; clarify that this check is not runtime containment.

197-198: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

심볼릭 링크 거부가 처리되지 않은 예외로 전파됩니다.

validate_repository_symlinksValueError를 발생시킵니다. main()try 블록은 이 예외를 처리하지 않습니다. finallyemit_result는 실행되지만 exit_code는 초기값 1로 남고, 그다음 예외가 main() 밖으로 전파됩니다. 호출자는 명확한 종료 코드 대신 traceback을 받습니다.

동작은 fail-closed이므로 보안 결함은 아닙니다. 다만 이 PR이 출력 제한에 대해 123을 도입한 방식과 일관되게, 경로 경계 거부에도 명시적 메시지와 안정적인 종료 코드를 부여하십시오. 그러면 방출되는 증거의 exit_code가 일반 실패와 경계 거부를 구분합니다.

♻️ 제안 리팩터
     copied_repo = sandbox / "repo"
     try:
-        copied_repo = copy_workspace(Path(args.repo_root), sandbox, args.ignore)
+        try:
+            copied_repo = copy_workspace(Path(args.repo_root), sandbox, args.ignore)
+        except ValueError as error:
+            print(f"sandboxed-verify: {error}", file=sys.stderr)
+            exit_code = PATH_BOUNDARY_EXIT_CODE
+            return exit_code
         env = scrubbed_env(sandbox, args.allow_env)

PATH_BOUNDARY_EXIT_CODE123, 124, 125와 충돌하지 않는 값으로 정의하고, 해당 값을 docs/doctoring/sandboxed-verification-symlink-boundary.mdCHANGELOG.md에 기록하십시오.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ci/sandboxed_verify.py` around lines 197 - 198, Handle the ValueError
raised by validate_repository_symlinks within main() so it does not escape after
emit_result; emit an explicit path-boundary rejection message and set a
dedicated stable PATH_BOUNDARY_EXIT_CODE distinct from 123, 124, and 125. Ensure
the emitted evidence records that code while preserving the existing
general-failure behavior, and document the new code in the specified
symlink-boundary guide and CHANGELOG.md.
tests/test_bounded_subprocess_contract.py (1)

26-32: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

전역 os 모듈에서 killpg를 삭제하면 같은 프로세스의 다른 테스트에 영향을 줄 수 있습니다.

bounded.os는 표준 라이브러리 os 모듈 자체입니다. monkeypatch.delattr(bounded.os, "killpg")는 테스트 동안 os.killpg를 프로세스 전역에서 제거합니다. bounded_subprocess의 drain 스레드는 데몬 스레드이고 오버플로 시 kill_process_group을 통해 os.killpg를 호출합니다. 이전 테스트에서 남은 데몬 스레드가 이 구간에서 실행되면 AttributeError가 발생합니다.

hasattr 검사를 모듈 수준 헬퍼로 감싸고 그 헬퍼를 패치하면 전역 변경 없이 같은 fail-closed 경로를 검증할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_bounded_subprocess_contract.py` around lines 26 - 32, Update
test_supported_platform_requires_posix_killpg and the platform-support check to
use a module-level helper that reports whether os.killpg is available, then
monkeypatch that helper to return false instead of deleting killpg from the
global os module. Preserve the expected OutputLimitUnsupportedError fail-closed
behavior.
tests/test_sandboxed_web_e2e_branch_contract.py (1)

112-122: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

정확한 경계값 4096에 대한 어서션을 추가하십시오.

현재 테스트는 4바이트와 4097바이트만 검증합니다. service_output_limitedst_size > log_limit_bytes를 사용합니다. 비교 연산자가 >=로 바뀌어도 이 테스트는 통과합니다. 정확히 한도와 같은 크기를 추가하면 off-by-one 회귀를 잡습니다.

♻️ 제안 보강
     service.log_path.write_bytes(b"safe")
     assert not sandboxed_web_e2e.service_output_limited(service)
+    service.log_path.write_bytes(b"x" * 4096)
+    assert not sandboxed_web_e2e.service_output_limited(service)
     service.log_path.write_bytes(b"x" * 4097)
     assert sandboxed_web_e2e.service_output_limited(service)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_sandboxed_web_e2e_branch_contract.py` around lines 112 - 122,
Extend test_service_limit_fallback_handles_missing_small_and_large_files to
write exactly 4096 bytes and assert service_output_limited(service) is false,
while preserving the existing checks for smaller and larger files.
scripts/ci/sandboxed_web_e2e.py (2)

253-265: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

max_lines 검증을 파일 존재 검사보다 먼저 수행하십시오.

현재 path.exists()가 False이면 max_lines 검증을 건너뜁니다. 그 결과 tail_text(missing_path, max_lines=0)은 예외 없이 빈 문자열을 반환합니다. 같은 잘못된 입력이 파일 존재 여부에 따라 다르게 처리됩니다. 인자 검증을 함수 시작부로 옮기면 계약이 일관됩니다.

♻️ 제안 리팩터
 def tail_text(
     path: Path,
     max_lines: int = 80,
     max_bytes: int = DEFAULT_TAIL_BYTES,
 ) -> str:
     """Return final lines after a byte-bounded service evidence read."""
-    if not path.exists():
-        return ""
     if max_lines <= 0:
         raise ValueError("max_lines must be positive")
+    if not path.exists():
+        return ""
     bounded_text = bounded_subprocess.read_bounded_suffix(path, max_bytes)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ci/sandboxed_web_e2e.py` around lines 253 - 265, Move the max_lines
validation in tail_text before the path.exists() check so non-positive values
always raise ValueError, including for missing paths. Preserve the existing
empty-string result for nonexistent paths with valid arguments.

404-415: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

stop_service 실패 후 프로세스 그룹을 강제 종료하십시오.

stop_service가 예외를 던지면 루프는 해당 서비스를 건너뛰고 다음 서비스로 진행합니다. 이때 해당 서비스의 자식 프로세스 그룹은 종료되지 않은 상태로 남을 수 있습니다. 라인 441에서 sandbox 디렉터리는 삭제되지만 프로세스는 계속 실행됩니다. CI 러너에 고아 프로세스가 누적됩니다.

예외 경로에서 bounded_subprocess.kill_process_group을 최선 노력으로 한 번 더 호출하면 이 누수가 닫힙니다.

♻️ 제안 보강
         for service in reversed(services):
             try:
                 stop_service(service)
             except (OSError, RuntimeError, subprocess.SubprocessError):
+                with contextlib.suppress(OSError, subprocess.SubprocessError):
+                    bounded_subprocess.kill_process_group(service.process)
                 output_limited = True
                 if exit_code != 124:
                     exit_code = bounded_subprocess.OUTPUT_LIMIT_EXIT_CODE
                 print(
                     "sandboxed-web-e2e: bounded service capture failed",
                     file=sys.stderr,
                 )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@scripts/ci/sandboxed_web_e2e.py` around lines 404 - 415, Update the exception
path in the finally cleanup loop around stop_service to make a best-effort
bounded_subprocess.kill_process_group call for the failed service before
continuing to the next service. Preserve the existing exception handling and
exit-code behavior, and ensure cleanup errors from the fallback kill do not
interrupt cleanup of remaining services.
tests/test_sandboxed_verify_output_limits.py (1)

161-179: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

두 CLI 예산 거부 테스트가 for 루프로 여러 케이스를 검증합니다. 공통 근본 원인은 파라미터화 대신 루프를 사용한 점입니다. 루프에서는 첫 케이스가 실패하면 나머지 케이스가 실행되지 않고, 실패 보고에 어느 값이 문제인지 나타나지 않습니다.

  • tests/test_sandboxed_verify_output_limits.py#L161-L179: for value in [...] 루프를 @pytest.mark.parametrize("value", ["4095", str(bounded.MAXIMUM_OUTPUT_LIMIT_BYTES + 1)])로 바꾸고 본문에서 단일 값만 검증하십시오.
  • tests/test_sandboxed_web_e2e_output_limits.py#L241-L264: for option, value in [...] 루프를 @pytest.mark.parametrize("option, value", [...])로 바꾸고 본문에서 단일 (option, value) 쌍만 검증하십시오.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_sandboxed_verify_output_limits.py` around lines 161 - 179, Replace
the loop in tests/test_sandboxed_verify_output_limits.py:161-179 with
`@pytest.mark.parametrize`("value", ["4095",
str(bounded.MAXIMUM_OUTPUT_LIMIT_BYTES + 1)]) and have the test validate one
value per invocation. Also replace the loop in
tests/test_sandboxed_web_e2e_output_limits.py:241-264 with
`@pytest.mark.parametrize`("option, value", [...]) so each option/value pair runs
as a separate test case with clear failure attribution.
tests/test_sandboxed_web_e2e_output_limits.py (2)

42-65: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

4096 한도와 4097 상한 검사가 두 테스트에 리터럴로 중복됩니다. 공통 근본 원인은 한도 + 1바이트 계약이 이름 없는 리터럴로 표현된 점입니다. 한도를 변경하면 각 테스트에서 두 개의 리터럴을 함께 수정해야 하고, 한쪽만 수정하면 어서션이 조용히 느슨해집니다.

  • tests/test_sandboxed_web_e2e_output_limits.py#L42-L65: 지역 변수 log_limit_bytes = 4096을 도입하고 라인 58의 인자와 라인 62의 <= log_limit_bytes + 1 검사에 사용하십시오.
  • tests/test_sandboxed_web_e2e_output_limits.py#L267-L301: 같은 지역 변수를 도입하고 라인 289의 --service-log-limit-bytes 값과 라인 299의 <= log_limit_bytes + 1 검사에 사용하십시오.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_sandboxed_web_e2e_output_limits.py` around lines 42 - 65,
Introduce a local log_limit_bytes = 4096 in
test_sandboxed_web_e2e_output_limits.py at lines 42-65 and use it for the
start_service limit argument and the <= log_limit_bytes + 1 assertion. Apply the
same local variable and substitutions at lines 267-301 for the
--service-log-limit-bytes argument and size assertion.

144-175: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

서비스 준비 출력에 대한 동기화 지점이 없습니다. 테스트가 불안정할 수 있습니다.

라인 171과 172는 서비스 로그 tail에 backend-readyfrontend-ready가 나타난다고 단언합니다. 그러나 이 실행은 --backend-ready-url--frontend-ready-url을 지정하지 않습니다. 따라서 wait_for_url은 즉시 True를 반환하고 main은 곧바로 E2E 명령을 실행한 뒤 정리 단계로 진입합니다.

서비스 자식은 별도의 Python 인터프리터입니다. 부하가 높은 러너에서 인터프리터 시작이 늦어지면 print 실행 전에 stop_service가 SIGTERM을 보냅니다. 그 경우 로그가 비어 있고 어서션이 실패합니다.

준비 URL을 사용하거나, 서비스가 준비 표시를 기록할 때까지 로그 파일을 폴링하면 이 경쟁 조건이 제거됩니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_sandboxed_web_e2e_output_limits.py` around lines 144 - 175, Update
test_normal_services_and_e2e_preserve_existing_success_contract to synchronize
service startup before running the E2E command: either provide reachable
--backend-ready-url and --frontend-ready-url values or poll each service log
until backend-ready and frontend-ready are recorded. Preserve the existing
success and payload assertions while ensuring both readiness messages are
emitted before cleanup can terminate the child processes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Around line 16-17: Under the “### Fixed” section of CHANGELOG.md, add a
changelog entry for the security boundary introduced by
validate_repository_symlinks: reject repository symlinks that resolve outside
the copied verification workspace, including absolute links, causing
verification to fail.

In `@scripts/ci/bounded_subprocess.py`:
- Around line 322-341: Update _cleanup_capture_startup_failure to close only
streams not owned by successfully started captures, rather than closing every
stream in streams while reader threads may still be active. Use each
BoundedOutputCapture’s stream ownership (adding a read-only stream property if
needed) to exclude captured streams from the close loop, while preserving the
existing cleanup and join behavior.

In `@scripts/ci/sandboxed_verify.py`:
- Around line 298-304: In main(), stop setting output_limited for the
OutputLimitUnsupportedError path because this exception indicates platform
capability failure, not an output-budget breach. Initialize
output_limit_unsupported to False, set it to True in that exception handler, and
pass it to emit_result under a separate evidence field while preserving the
existing fail-closed exit code.

In `@tests/test_sandboxed_verify_symlink_boundary.py`:
- Around line 64-82: 보이지 않는 증거 방출을 검증하도록
test_timeout_without_partial_streams_still_emits_failed_evidence를 수정하십시오.
capsys로 SANDBOXED_VERIFY_RESULT 출력 행을 읽고 JSON으로 파싱한 뒤, 실패 결과 페이로드의 구조와
output_limit_bytes 및 output_limited 필드를 포함한 정확한 값을 단언하십시오. 이를 위해 json을 import하고
기존 반환 코드 124 단언은 유지하십시오.

---

Nitpick comments:
In `@scripts/ci/bounded_subprocess.py`:
- Around line 109-117: The suffix_budget zero branch in _render_bounded_bytes is
unreachable after validate_output_limit enforces MINIMUM_OUTPUT_LIMIT_BYTES.
Remove the conditional fallback and construct the suffix using the
guaranteed-positive budget, preserving the marker-plus-suffix truncation
behavior and 100% branch coverage.

In `@scripts/ci/sandboxed_verify.py`:
- Line 288: Update the assignment in the completed-process handling path to
access the guaranteed output_limited field directly on completed instead of
using getattr with a fallback. Leave the getattr usage in the TimeoutExpired
exception handler unchanged, since that handler receives a base exception
without this field.
- Around line 159-188: Document the runtime symlink limitation in
docs/doctoring/sandboxed-verification-symlink-boundary.md: explain that
validate_repository_symlinks performs only a pre-execution scan, so an untrusted
verification command may create and follow an escaping symlink during execution;
clarify that this check is not runtime containment.
- Around line 197-198: Handle the ValueError raised by
validate_repository_symlinks within main() so it does not escape after
emit_result; emit an explicit path-boundary rejection message and set a
dedicated stable PATH_BOUNDARY_EXIT_CODE distinct from 123, 124, and 125. Ensure
the emitted evidence records that code while preserving the existing
general-failure behavior, and document the new code in the specified
symlink-boundary guide and CHANGELOG.md.

In `@scripts/ci/sandboxed_web_e2e.py`:
- Around line 253-265: Move the max_lines validation in tail_text before the
path.exists() check so non-positive values always raise ValueError, including
for missing paths. Preserve the existing empty-string result for nonexistent
paths with valid arguments.
- Around line 404-415: Update the exception path in the finally cleanup loop
around stop_service to make a best-effort bounded_subprocess.kill_process_group
call for the failed service before continuing to the next service. Preserve the
existing exception handling and exit-code behavior, and ensure cleanup errors
from the fallback kill do not interrupt cleanup of remaining services.

In `@tests/test_bounded_subprocess_contract.py`:
- Around line 26-32: Update test_supported_platform_requires_posix_killpg and
the platform-support check to use a module-level helper that reports whether
os.killpg is available, then monkeypatch that helper to return false instead of
deleting killpg from the global os module. Preserve the expected
OutputLimitUnsupportedError fail-closed behavior.

In `@tests/test_sandboxed_verify_output_limits.py`:
- Around line 161-179: Replace the loop in
tests/test_sandboxed_verify_output_limits.py:161-179 with
`@pytest.mark.parametrize`("value", ["4095",
str(bounded.MAXIMUM_OUTPUT_LIMIT_BYTES + 1)]) and have the test validate one
value per invocation. Also replace the loop in
tests/test_sandboxed_web_e2e_output_limits.py:241-264 with
`@pytest.mark.parametrize`("option, value", [...]) so each option/value pair runs
as a separate test case with clear failure attribution.

In `@tests/test_sandboxed_web_e2e_branch_contract.py`:
- Around line 112-122: Extend
test_service_limit_fallback_handles_missing_small_and_large_files to write
exactly 4096 bytes and assert service_output_limited(service) is false, while
preserving the existing checks for smaller and larger files.

In `@tests/test_sandboxed_web_e2e_output_limits.py`:
- Around line 42-65: Introduce a local log_limit_bytes = 4096 in
test_sandboxed_web_e2e_output_limits.py at lines 42-65 and use it for the
start_service limit argument and the <= log_limit_bytes + 1 assertion. Apply the
same local variable and substitutions at lines 267-301 for the
--service-log-limit-bytes argument and size assertion.
- Around line 144-175: Update
test_normal_services_and_e2e_preserve_existing_success_contract to synchronize
service startup before running the E2E command: either provide reachable
--backend-ready-url and --frontend-ready-url values or poll each service log
until backend-ready and frontend-ready are recorded. Preserve the existing
success and payload assertions while ensuring both readiness messages are
emitted before cleanup can terminate the child processes.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 01f67cd3-3b3c-4a93-9c08-53fb8c91dc3e

📥 Commits

Reviewing files that changed from the base of the PR and between 6eb06cd and 52e1281.

📒 Files selected for processing (17)
  • CHANGELOG.md
  • docs/doctoring/sandboxed-output-resource-bounds.md
  • docs/doctoring/sandboxed-verification-symlink-boundary.md
  • scripts/ci/bounded_subprocess.py
  • scripts/ci/sandboxed_verify.py
  • scripts/ci/sandboxed_web_e2e.py
  • tests/test_bounded_subprocess.py
  • tests/test_bounded_subprocess_capture_startup.py
  • tests/test_bounded_subprocess_contract.py
  • tests/test_repository_branch_coverage_execution_sandboxes.py
  • tests/test_sandboxed_entrypoint_and_cleanup_coverage.py
  • tests/test_sandboxed_service_capture_startup.py
  • tests/test_sandboxed_verify_output_limits.py
  • tests/test_sandboxed_verify_symlink_boundary.py
  • tests/test_sandboxed_web_e2e.py
  • tests/test_sandboxed_web_e2e_branch_contract.py
  • tests/test_sandboxed_web_e2e_output_limits.py

Comment thread CHANGELOG.md
Comment thread scripts/ci/bounded_subprocess.py
Comment thread scripts/ci/sandboxed_verify.py
Comment thread tests/test_sandboxed_verify_symlink_boundary.py

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

Re-evaluate exact current head dd2132b5aedc56859ffa81568d42ab9e81e52b65 against independently resolved live main 6eb06cdd08c79a06f7b390069d4ffa49e2eb7dba. All ten pull-request workflows on this head are terminal-success and prior actionable threads are resolved. Do not transfer predecessor-head review state or infer qualifying independent human approval or merge authority.

@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

@seonghobae I will review exact head dd2132b5aedc56859ffa81568d42ab9e81e52b65 against main 6eb06cdd08c79a06f7b390069d4ffa49e2eb7dba.

I will evaluate this head independently. I will keep semantic source findings separate from workflow and policy status. This review does not infer independent human approval or merge authority.

⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Copy Markdown
Contributor Author

@opencode-agent review

Evaluate exact current head dd2132b5aedc56859ffa81568d42ab9e81e52b65 against independently resolved live main 6eb06cdd08c79a06f7b390069d4ffa49e2eb7dba. All ten observed exact-head workflows are terminal-success, GitHub reports the Draft head mergeable, and unresolved threads are zero. Review the copied-workspace symlink boundary, bounded subprocess/service-output behavior, cleanup, and exit-code precedence only on this head. Submit a semantic verdict without changing Draft state; keep automated judgment separate from qualifying independent human approval, merge authority, and protected-main operational acceptance.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant